1   /*                        __    __  __  __    __  ___
2    *                       \  \  /  /    \  \  /  /  __/
3    *                        \  \/  /  /\  \  \/  /  /
4    *                         \____/__/  \__\____/__/.ɪᴏ
5    * ᶜᵒᵖʸʳᶦᵍʰᵗ ᵇʸ ᵛᵃᵛʳ ⁻ ˡᶦᶜᵉⁿˢᵉᵈ ᵘⁿᵈᵉʳ ᵗʰᵉ ᵃᵖᵃᶜʰᵉ ˡᶦᶜᵉⁿˢᵉ ᵛᵉʳˢᶦᵒⁿ ᵗʷᵒ ᵈᵒᵗ ᶻᵉʳᵒ
6    */
7   package io.vavr;
8   
9   import io.vavr.control.Either;
10  import io.vavr.match.annotation.Patterns;
11  import io.vavr.collection.List;
12  import io.vavr.control.Option;
13  import io.vavr.control.Option.Some;
14  import io.vavr.match.annotation.Unapply;
15  import org.junit.Test;
16  
17  import java.math.BigDecimal;
18  import java.time.Year;
19  import java.util.function.Predicate;
20  
21  import static io.vavr.API.*;
22  import static io.vavr.Patterns.*;
23  import static io.vavr.MatchTest_DeveloperPatterns.$Developer;
24  import static io.vavr.Predicates.*;
25  import static org.assertj.core.api.Assertions.assertThat;
26  
27  public class MatchTest {
28  
29      // -- MatchError
30  
31      @Test(expected = MatchError.class)
32      public void shouldThrowIfNotMatching() {
33          Match(new Object()).of(
34                  Case($(ignored -> false), o -> null)
35          );
36      }
37  
38      // -- $()
39  
40      @Test
41      public void shouldMatchNullWithAnyReturningValue() {
42          final Match.Case<Object, Integer> _case = Case($(), 1);
43          final Object obj = null;
44          assertThat(_case.isDefinedAt(obj)).isTrue();
45          assertThat(_case.apply(obj)).isEqualTo(1);
46      }
47  
48      @Test
49      public void shouldMatchAnyReturningValue() {
50          final Match.Case<Object, Integer> _case = Case($(), 1);
51          final Object obj = new Object();
52          assertThat(_case.isDefinedAt(obj)).isTrue();
53          assertThat(_case.apply(obj)).isEqualTo(1);
54      }
55  
56      @Test
57      public void shouldMatchNullWithAnyReturningAppliedFunction() {
58          final Match.Case<Object, Integer> _case = Case($(), o -> 1);
59          final Object obj = null;
60          assertThat(_case.isDefinedAt(obj)).isTrue();
61          assertThat(_case.apply(obj)).isEqualTo(1);
62      }
63  
64      @Test
65      public void shouldMatchAnyReturningAppliedFunction() {
66          final Match.Case<Object, Integer> _case = Case($(), o -> 1);
67          final Object obj = new Object();
68          assertThat(_case.isDefinedAt(obj)).isTrue();
69          assertThat(_case.apply(obj)).isEqualTo(1);
70      }
71  
72      @Test
73      public void shouldTakeFirstMatch() {
74          final String actual = Match(new Object()).of(
75                  Case($(), "first"),
76                  Case($(), "second")
77          );
78          assertThat(actual).isEqualTo("first");
79      }
80  
81      // -- $(value)
82  
83      @Test
84      public void shouldMatchValueReturningValue() {
85          final Object obj = new Object();
86          final Match.Case<Object, Integer> _case = Case($(obj), 1);
87          assertThat(_case.isDefinedAt(obj)).isTrue();
88          assertThat(_case.apply(obj)).isEqualTo(1);
89      }
90  
91      @Test
92      public void shouldMatchValueReturningValue_NegativeCase() {
93          final Object obj = new Object();
94          final Match.Case<Object, Integer> _case = Case($(obj), 1);
95          assertThat(_case.isDefinedAt(new Object())).isFalse();
96      }
97  
98      @Test
99      public void shouldMatchValueReturningAppliedFunction() {
100         final Object obj = new Object();
101         final Match.Case<Object, Integer> _case = Case($(obj), o -> 1);
102         assertThat(_case.isDefinedAt(obj)).isTrue();
103         assertThat(_case.apply(obj)).isEqualTo(1);
104     }
105 
106     @Test
107     public void shouldMatchValueReturningAppliedFunction_NegativeCase() {
108         final Object obj = new Object();
109         final Match.Case<Object, Integer> _case = Case($(obj), o -> 1);
110         assertThat(_case.isDefinedAt(new Object())).isFalse();
111     }
112 
113     // -- $(predicate)
114 
115     @Test
116     public void shouldMatchPredicateReturningValue() {
117         final Object obj = new Object();
118         final Match.Case<Object, Integer> _case = Case($(is(obj)), 1);
119         assertThat(_case.isDefinedAt(obj)).isTrue();
120         assertThat(_case.apply(obj)).isEqualTo(1);
121     }
122 
123     @Test
124     public void shouldMatchPredicateReturningValue_NegativeCase() {
125         final Object obj = new Object();
126         final Match.Case<Object, Integer> _case = Case($(is(obj)), 1);
127         assertThat(_case.isDefinedAt(new Object())).isFalse();
128     }
129 
130     @Test
131     public void shouldMatchPredicateReturningAppliedFunction() {
132         final Object obj = new Object();
133         final Match.Case<Object, Integer> _case = Case($(is(obj)), o -> 1);
134         assertThat(_case.isDefinedAt(obj)).isTrue();
135         assertThat(_case.apply(obj)).isEqualTo(1);
136     }
137 
138     @Test
139     public void shouldMatchPredicateReturningAppliedFunction_NegativeCase() {
140         final Object obj = new Object();
141         final Match.Case<Object, Integer> _case = Case($(is(obj)), o -> 1);
142         assertThat(_case.isDefinedAt(new Object())).isFalse();
143     }
144 
145     // -- multiple cases
146 
147     // i match {
148     //     case 1 => "one"
149     //     case 2 => "two"
150     //     case _ => "many"
151     // }
152 
153     @Test
154     public void shouldMatchIntUsingPatterns() {
155         final String actual = Match(3).of(
156                 Case($(1), "one"),
157                 Case($(2), "two"),
158                 Case($(), "many")
159         );
160         assertThat(actual).isEqualTo("many");
161     }
162 
163     @Test
164     public void shouldMatchIntUsingPredicates() {
165         final String actual = Match(3).of(
166                 Case($(is(1)), "one"),
167                 Case($(is(2)), "two"),
168                 Case($(), "many")
169         );
170         assertThat(actual).isEqualTo("many");
171     }
172 
173     @Test
174     public void shouldComputeUpperBoundOfReturnValue() {
175         final Number num = Match(3).of(
176                 Case($(is(1)), 1),
177                 Case($(is(2)), 2.0),
178                 Case($(), i -> new BigDecimal("" + i))
179         );
180         assertThat(num).isEqualTo(new BigDecimal("3"));
181     }
182 
183     // -- instanceOf
184 
185     @Test
186     public void shouldMatchUsingInstanceOf() {
187         final Object obj = 1;
188         final int actual = Match(obj).of(
189                 Case($(instanceOf(Year.class)), y -> 0),
190                 Case($(instanceOf(Integer.class)), i -> 1)
191         );
192         assertThat(actual).isEqualTo(1);
193     }
194 
195     // -- Either
196 
197     @Test
198     public void shouldMatchLeft() {
199         final Either<Integer, String> either = Either.left(1);
200         final String actual = Match(either).of(
201                 Case($Left($()), l -> "left: " + l),
202                 Case($Right($()), r -> "right: " + r)
203         );
204         assertThat(actual).isEqualTo("left: 1");
205     }
206 
207     @Test
208     public void shouldMatchRight() {
209         final Either<Integer, String> either = Either.right("a");
210         final String actual = Match(either).of(
211                 Case($Left($()), l -> "left: " + l),
212                 Case($Right($()), r -> "right: " + r)
213         );
214         assertThat(actual).isEqualTo("right: a");
215     }
216 
217     // -- Option
218 
219     @Test
220     public void shouldMatchSome() {
221         final Option<Integer> opt = Option.some(1);
222         final String actual = Match(opt).of(
223                 Case($None(), "no value"),
224                 Case($Some($()), String::valueOf)
225         );
226         assertThat(actual).isEqualTo("1");
227     }
228 
229     @Test
230     public void shouldMatchNone() {
231         final Option<Integer> opt = Option.none();
232         final String actual = Match(opt).of(
233                 Case($Some($()), String::valueOf),
234                 Case($None(), "no value")
235         );
236         assertThat(actual).isEqualTo("no value");
237     }
238 
239     @Test
240     public void shouldDecomposeSomeTuple() {
241         final Option<Tuple2<String, Integer>> tuple2Option = Option.of(Tuple.of("Test", 123));
242         final Tuple2<String, Integer> actual = Match(tuple2Option).of(
243                 Case($Some($()), value -> {
244                     @SuppressWarnings("UnnecessaryLocalVariable")
245                     final Tuple2<String, Integer> tuple2 = value; // types are inferred correctly!
246                     return tuple2;
247                 })
248         );
249         assertThat(actual).isEqualTo(Tuple.of("Test", 123));
250     }
251 
252     @Test
253     public void shouldDecomposeSomeSomeTuple() {
254         final Option<Option<Tuple2<String, Integer>>> tuple2OptionOption = Option.of(Option.of(Tuple.of("Test", 123)));
255         final Some<Tuple2<String, Integer>> actual = Match(tuple2OptionOption).of(
256                 Case($Some($Some($(Tuple.of("Test", 123)))), value -> {
257                     @SuppressWarnings("UnnecessaryLocalVariable")
258                     final Some<Tuple2<String, Integer>> some = value; // types are inferred correctly!
259                     return some;
260                 })
261         );
262         assertThat(actual).isEqualTo(Option.of(Tuple.of("Test", 123)));
263     }
264 
265     // -- List
266 
267     @Test
268     public void shouldDecomposeEmptyList() {
269         final List<Integer> list = List.empty();
270         final boolean isEmpty = Match(list).of(
271                 Case($Cons($(), $()), (x, xs) -> false),
272                 Case($Nil(), true)
273         );
274         assertThat(isEmpty).isTrue();
275     }
276 
277     @Test
278     public void shouldDecomposeNonEmptyList() {
279         final List<Integer> list = List.of(1);
280         final boolean isNotEmpty = Match(list).of(
281                 Case($Nil(), false),
282                 Case($Cons($(), $()), (x, xs) -> true)
283         );
284         assertThat(isNotEmpty).isTrue();
285     }
286 
287     @SuppressWarnings("UnnecessaryLocalVariable")
288     @Test
289     public void shouldDecomposeListOfTuple3() {
290         final List<Tuple3<String, Integer, Double>> tuple3List = List.of(
291                 Tuple.of("begin", 10, 4.5),
292                 Tuple.of("middle", 11, 0.0),
293                 Tuple.of("end", 12, 1.2));
294         final String actual = Match(tuple3List).of(
295                 Case($Cons($(), $()), (x, xs) -> {
296                     // types are inferred correctly!
297                     final Tuple3<String, Integer, Double> head = x;
298                     final List<Tuple3<String, Integer, Double>> tail = xs;
299                     return head + "::" + tail;
300                 })
301         );
302         assertThat(actual).isEqualTo("(begin, 10, 4.5)::List((middle, 11, 0.0), (end, 12, 1.2))");
303     }
304 
305     @SuppressWarnings("UnnecessaryLocalVariable")
306     @Test
307     public void shouldDecomposeListWithNonEmptyTail() {
308         final List<Option<Number>> intOptionList = List.of(Option.some(1), Option.some(2.0));
309         final String actual = Match(intOptionList).of(
310                 Case($Cons($Some($(1)), $Cons($Some($(2.0)), $())), (x, xs) -> {
311                     // types are inferred correctly!
312                     final Some<Number> head = x;
313                     final List<Option<Number>> tail = xs;
314                     return head + "::" + tail;
315                 })
316         );
317         assertThat(actual).isEqualTo("Some(1)::List(Some(2.0))");
318     }
319 
320     // -- run
321 
322     @Test
323     public void shouldRunUnitOfWork() {
324 
325         class OuterWorld {
326 
327             String effect = null;
328 
329             void displayHelp() {
330                 effect = "help";
331             }
332 
333             void displayVersion() {
334                 effect = "version";
335             }
336         }
337 
338         final OuterWorld outerWorld = new OuterWorld();
339 
340         Match("-v").of(
341                 Case($(isIn("-h", "--help")), o -> run(outerWorld::displayHelp)),
342                 Case($(isIn("-v", "--version")), o -> run(outerWorld::displayVersion)),
343                 Case($(), o -> { throw new IllegalArgumentException(); })
344         );
345 
346         assertThat(outerWorld.effect).isEqualTo("version");
347     }
348 
349     @Test
350     public void shouldRunWithInferredArguments() {
351 
352         class OuterWorld {
353 
354             Number effect = null;
355 
356             void writeInt(int i) {
357                 effect = i;
358             }
359 
360             void writeDouble(double d) {
361                 effect = d;
362             }
363         }
364 
365         final OuterWorld outerWorld = new OuterWorld();
366         final Object obj = .1d;
367 
368         Match(obj).of(
369                 Case($(instanceOf(Integer.class)), i -> run(() -> outerWorld.writeInt(i))),
370                 Case($(instanceOf(Double.class)), d -> run(() -> outerWorld.writeDouble(d))),
371                 Case($(), o -> { throw new NumberFormatException(); })
372         );
373 
374         assertThat(outerWorld.effect).isEqualTo(.1d);
375     }
376 
377     // -- Developer
378 
379     @Test
380     public void shouldMatchCustomTypeWithUnapplyMethod() {
381         final Person person = new Developer("Daniel", true, Option.some(13));
382         final String actual = Match(person).of(
383                 Case($Developer($("Daniel"), $(true), $()), Person.Util::devInfo),
384                 Case($(), p -> "Unknown person: " + p.getName())
385         );
386         assertThat(actual).isEqualTo("Daniel is caffeinated.");
387     }
388 
389     interface Person {
390         String getName();
391 
392         class Util {
393             static String devInfo(String name, boolean isCaffeinated, Option<Number> number) {
394                 return name + " is " + (isCaffeinated ? "" : "not ") + "caffeinated.";
395             }
396         }
397     }
398 
399     static final class Developer implements Person {
400         private final String name;
401         private final boolean isCaffeinated;
402         private final Option<Number> number;
403 
404         Developer(String name, boolean isCaffeinated, Option<Number> number) {
405             this.name = name;
406             this.isCaffeinated = isCaffeinated;
407             this.number = number;
408         }
409 
410         public String getName() { return name; }
411 
412         public boolean isCaffeinated() { return isCaffeinated; }
413 
414         public Option<Number> number() { return number; }
415 
416         @Patterns
417         static class $ {
418             @Unapply
419             static Tuple3<String, Boolean, Option<Number>> Developer(Developer dev) {
420                 return Tuple.of(dev.getName(), dev.isCaffeinated(), dev.number());
421             }
422         }
423     }
424 
425     // Ambiguity check
426 
427     @Test
428     public void shouldNotAmbiguous() {
429 
430         { // value
431             // Case("1", o -> "ok"); // Not possible, would lead to ambiguities (see below)
432             assertThat(Case($("1"), () -> "ok").apply("1")).isEqualTo("ok");
433             assertThat(Case($("1"), "ok").apply("1")).isEqualTo("ok");
434         }
435 
436         { // predicate as variable
437             Predicate<String> p = s -> true;
438             assertThat(Case($(p), o -> "ok").apply("1")).isEqualTo("ok"); // ambiguous, if Case(T, Function<T, R>) present
439             assertThat(Case($(p), () -> "ok").apply("1")).isEqualTo("ok");
440             assertThat(Case($(p), "ok").apply("1")).isEqualTo("ok");
441         }
442 
443         { // $(predicate)
444             assertThat(Case($(o -> true), o -> "ok").apply("1")).isEqualTo("ok"); // ambiguous, if Case(T, Function<T, R>) present
445             assertThat(Case($(o -> true), () -> "ok").apply("1")).isEqualTo("ok");
446             assertThat(Case($(o -> true), "ok").apply("1")).isEqualTo("ok");
447         }
448 
449         { // $(value)
450             assertThat(Case($("1"), o -> "ok").apply("1")).isEqualTo("ok"); // ambiguous, if Case(T, Function<T, R>) present
451             assertThat(Case($("1"), () -> "ok").apply("1")).isEqualTo("ok");
452             assertThat(Case($("1"), "ok").apply("1")).isEqualTo("ok");
453         }
454     }
455 }